package cn.song.controller;

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.HttpUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.GZIPInputStream;

/**
 * 请求转发控制器
 * 提供统一的HTTP请求转发功能，支持所有标准HTTP方法及文件上传。
 * 通过targetBaseUrl参数指定目标服务基础地址，自动拼接原始请求路径进行转发。
 *
 * @author SongRenShuo
 * @date 2024/05/20
 */
@Slf4j
@RestController
public class ApiController {

    @Resource
    private WebClient webClient;

    /**
     * 目标URL
     */
    public final static String TARGET_BASE_URL = "targetBaseUrl";
    /**
     * 目标URL参数是否移除
     */
    public final static String TARGET_BASE_URL_REMOVE = "targetBaseUrlRemove";
    /** 文本文档 **/
    public static final String TEXT = "text";

    /**
     * 通用请求转发方法
     * 支持所有HTTP方法，自动处理表单数据、文件上传及响应编码。
     *
     * @param request    原始请求对象，用于获取请求路径和参数
     * @param headers    请求头信息（自动注入）
     * @param formParams 表单参数（包含文件上传时的文本字段）
     * @param body       请求体内容（非表单请求时使用）
     * @return {@link ResponseEntity }<{@link byte[] }> 转发目标服务的响应实体
     */
    @RequestMapping(value = "/**",
            method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT,
                    RequestMethod.DELETE, RequestMethod.PATCH, RequestMethod.OPTIONS,
                    RequestMethod.HEAD, RequestMethod.TRACE})
    public ResponseEntity<byte[]> forwardRequestSamePath(
            HttpServletRequest request,
            @RequestHeader HttpHeaders headers,
            @RequestParam MultiValueMap<String, String> formParams,
            @RequestBody(required = false) String body) {

        // 生成唯一请求标识用于日志追踪
        final UUID uuid = UUID.fastUUID();
        // 添加开始时间记录
        final long startTime = System.currentTimeMillis();

        String targetUrl = null;
        HttpMethod method = null;
        String formParamsString = null;
        try {
            // 1. 获取并校验目标服务基础地址
            final String targetBaseUrl = request.getParameter(TARGET_BASE_URL);
            if (targetBaseUrl == null) {
                throw new IllegalArgumentException("必须提供'" + TARGET_BASE_URL + "'请求参数");
            }

            // 2. 移除targetBaseUrl避免传递到下游服务
            String targetBaseUrlRemove = request.getParameter(TARGET_BASE_URL_REMOVE);
            if (Boolean.parseBoolean(targetBaseUrlRemove)) {
                body = removeTargetBaseUrl(targetBaseUrl, targetBaseUrlRemove, formParams, body);
            }

            // 3. 解析HTTP方法 - 如果是POST但有formParams且目标是GET接口，则转换为GET
            method = HttpMethod.resolve(request.getMethod());
            if (method == null) {
                throw new IllegalArgumentException("不支持的HTTP方法: " + request.getMethod());
            }

            // 4. 构建完整目标URL（保留原始请求路径）
            targetUrl = getTargetUrl(request, targetBaseUrl, formParams);

            // 5. 获取表单参数字符串
            formParamsString = getFormParamsString(formParams);

            // 6. 记录请求开始日志
            log.info("请求转发开始,uuid:{} | 目标URL: {} | 方法: {} | 请求数据body: {} | 请求数据formParams: {}\r\n | 头部信息: {}",
                    uuid, targetUrl, method, body, formParamsString, headers);

            // 7. 构建请求实体（处理multipart/form-data情况）
            HttpEntity<?> requestEntity = buildRequestEntity(request, headers, formParams, body);

            // 8. 执行请求转发
            long httpStartTime = System.currentTimeMillis();
            ResponseEntity<byte[]> response = executeForwardRequest(targetUrl, method, requestEntity);
            long httpEndTime = System.currentTimeMillis();

            // 9. 处理响应结果
            processResponse(uuid, targetUrl, method, response, headers, formParamsString, body, startTime, httpStartTime, httpEndTime);

            return response;
        } catch (IllegalArgumentException e) {
            return buildErrorResponse(uuid, startTime, e);
        } catch (HttpClientErrorException e) {
            return handleClientError(uuid, startTime, e, targetUrl, method, body, formParamsString);
        } catch (Throwable e) {
            return handleServerError(uuid, startTime, e, targetUrl, method, body, formParamsString);
        }
    }


    /**
     * 移除 targetBaseUrl
     *
     * @param targetBaseUrl 目标URL
     * @param targetBaseUrlRemove 目标URL参数是否移除
     * @param formParams 表单参数
     * @param body 请求体
     * @date 2025/03/23
     * @return {@link String }
     * @author SongRenShuo
     */
    @SneakyThrows
    private static String removeTargetBaseUrl(String targetBaseUrl, String targetBaseUrlRemove, MultiValueMap<String, String> formParams, String body) {
        // 移除 formParams中的targetBaseUrl
        formParams.remove(TARGET_BASE_URL);
        formParams.remove(TARGET_BASE_URL_REMOVE);
        // 移除 body中的targetBaseUrl
        if (body == null) {
            return null;
        }

        return body.replace(targetBaseUrl, "").replace(URLEncoder.encode(targetBaseUrl, StandardCharsets.UTF_8.name()), "")
                .replace("&" + TARGET_BASE_URL + "=", "").replace("?" + TARGET_BASE_URL + "=", "?")
                .replace("&" + TARGET_BASE_URL_REMOVE + "=" + targetBaseUrlRemove, "").replace("?" + TARGET_BASE_URL_REMOVE + "=" + targetBaseUrlRemove, "?");
    }

    /**
     * 构建完整目标URL
     *
     * @param request 请求
     * @param targetBaseUrl 目标基 URL
     * @param formParams 表单参数
     * @date 2025/03/24
     * @return {@link String }
     * @author SongRenShuo
     */
    private static String getTargetUrl(HttpServletRequest request, String targetBaseUrl, MultiValueMap<String, String> formParams) {
        // 构建目标URL
        String targetUrl = targetBaseUrl + request.getRequestURI().substring(request.getContextPath().length());

        // 附加表单参数到URL查询参数（对于GET请求或需要将表单参数转换为查询参数的情况）
        if (!formParams.isEmpty()) {
            // 使用fromHttpUrl替代fromUriString提高安全性
            Map<String, List<String>> existingQueryParams = HttpUtil.decodeParams(targetUrl, StandardCharsets.UTF_8);
            // 移除targetUrl
            existingQueryParams.remove(targetUrl);

            // 创建一个新的MultiValueMap实例，避免直接修改不可修改的existingQueryParams
            MultiValueMap<String, String> combinedParams = new LinkedMultiValueMap<>(existingQueryParams);
            combinedParams.putAll(formParams);

            // 使用false作为最后一个参数，避免对参数进行二次编码
            targetUrl = HttpUtil.urlWithForm(targetUrl, HttpUtil.toParams(combinedParams), StandardCharsets.UTF_8, false);
        }

        return targetUrl;
    }


    /**
     * 获取表单参数字符串
     *
     * @param formParams 表单参数
     * @date 2025/03/24
     * @return {@link String }
     * @author SongRenShuo
     */
    private static String getFormParamsString(MultiValueMap<String, String> formParams) {
        return formParams.entrySet().stream()
                .flatMap(entry -> {
                    // 检查 entry.getValue() 是否为 null
                    if (entry.getValue() == null || entry.getValue().isEmpty()) {
                        // 如果为空，返回一个空流
                        return Stream.empty();
                    }
                    return entry.getValue().stream()
                            .map(value -> entry.getKey() + "=" + value);
                })
                .collect(Collectors.joining("&"));
    }

    /**
     * 构建请求实体
     * 根据请求类型处理普通表单/JSON数据或文件上传
     */
    private HttpEntity<?> buildRequestEntity(HttpServletRequest request,
                                             HttpHeaders headers,
                                             MultiValueMap<String, String> formParams,
                                             String body) throws IOException {
        // 移除可能引起问题的头部 - 避免代理服务器将请求转发给目标服务器时，目标服务器拒绝连接
        headers.remove("host");

        // 使用hutool解析目标URL并获取host
        String targetBaseUrl = request.getParameter(TARGET_BASE_URL);
        if (targetBaseUrl != null) {
            String host = URLUtil.getHost(URLUtil.url(targetBaseUrl)).getHost();
            headers.add("host", host);
        }

        // 处理multipart请求（文件上传）
        if (request instanceof MultipartHttpServletRequest) {
            MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
            MultiValueMap<String, Object> multipartBody = new LinkedMultiValueMap<>();

            // 处理文件部分
            for (Iterator<String> it = multipartRequest.getFileNames(); it.hasNext(); ) {
                String paramName = it.next();
                for (MultipartFile file : multipartRequest.getFiles(paramName)) {
                    multipartBody.add(paramName, createFileResource(file));
                }
            }

            // 处理文本表单字段 - 保持原始值，不进行URL编码
            formParams.forEach((key, values) ->
                    values.forEach(value -> {
                        // 对于multipart请求，直接添加原始值，不进行编码
                        multipartBody.add(key, value);
                    })
            );

            return new HttpEntity<>(multipartBody, headers);
        }
        // 处理普通请求
        else {
            // 如果有表单参数且Content-Type为application/x-www-form-urlencoded，则构建表单数据
            if (!formParams.isEmpty() &&
                    (headers.getContentType() == null || headers.getContentType().includes(MediaType.APPLICATION_FORM_URLENCODED))) {
                // 使用Hutool的toParams方法构建表单数据，但禁用自动编码
                String formData = HttpUtil.toParams(formParams, StandardCharsets.UTF_8, false);
                return new HttpEntity<>(formData, headers);
            }

            return new HttpEntity<>(body, headers);
        }
    }

    /**
     * 创建文件资源对象
     */
    private ByteArrayResource createFileResource(MultipartFile file) throws IOException {
        return new ByteArrayResource(file.getBytes()) {
            @Override
            public String getFilename() {
                // 保留原始文件名
                return file.getOriginalFilename();
            }
        };
    }

    /**
     * 执行实际请求转发
     */
    private ResponseEntity<byte[]> executeForwardRequest(String targetUrl,
                                                         HttpMethod method,
                                                         HttpEntity<?> requestEntity) {
        // 使用已配置的 WebClient 实例
        WebClient.RequestHeadersUriSpec<?> requestHeadersUriSpec = webClient.method(method);

        // 设置 URL 和 Headers
        WebClient.RequestHeadersSpec<?> requestHeadersSpec = requestHeadersUriSpec.uri(targetUrl)
                .headers(httpHeaders -> httpHeaders.addAll(requestEntity.getHeaders()));

        // 设置 Body (如果存在)
        if (requestEntity.getBody() != null) {
            requestHeadersSpec = ((WebClient.RequestBodySpec) requestHeadersSpec).bodyValue(requestEntity.getBody());
        }
        // 执行请求并处理响应，禁用自动重定向处理以正确返回302状态码
        return requestHeadersSpec.exchangeToMono(clientResponse -> {
            // 获取状态码和头部信息
            HttpStatus statusCode = clientResponse.statusCode();
            HttpHeaders responseHeaders = new HttpHeaders();
            responseHeaders.putAll(clientResponse.headers().asHttpHeaders());

            // 处理响应体
            return clientResponse.bodyToMono(byte[].class)
                    .defaultIfEmpty(new byte[0])
                    .map(body -> ResponseEntity.status(statusCode).headers(responseHeaders).body(body));
        }).block(); // 阻塞以获取响应，模拟同步行为
    }

    /**
     * 处理响应结果
     */
    private void processResponse(UUID uuid,
                                 String targetUrl,
                                 HttpMethod method,
                                 ResponseEntity<byte[]> response,
                                 HttpHeaders headers,
                                 String formParamsString,
                                 String body,
                                 long startTime,
                                 long httpStartTime,
                                 long httpEndTime) throws IOException {

        // 获取内容类型和编码
        String contentType = response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        Charset charset = parseCharset(contentType);

        // 处理gzip压缩内容
        boolean isGzip = "gzip".equalsIgnoreCase(
                response.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING));

        String responseBody = parseResponseBody(response.getBody(), isGzip, charset);

        // 计算耗时
        long totalTime = System.currentTimeMillis() - startTime;
        long httpTime = httpEndTime - httpStartTime;
        long programTime = totalTime - httpTime;

        // 记录请求完成日志
        log.info("请求转发结束,uuid:{} | 目标URL: {} | 方法: {} | 请求数据body: {} | 请求数据formParams: {}\r\n | 头部信息: {}\r\n | 响应: {}\r\n | 程序处理耗时: {} ms | 接口请求耗时: {} ms | 总耗时: {} ms",
                uuid, targetUrl, method, body, formParamsString, headers, responseBody, programTime, httpTime, totalTime);
    }

    /**
     * 解析响应正文
     */
    private String parseResponseBody(byte[] body, boolean isGzip, Charset charset)
            throws IOException {
        if (body == null) {
            return "";
        }

        if (isGzip) {
            try (GZIPInputStream gzipInput = new GZIPInputStream(new ByteArrayInputStream(body));
                 BufferedReader reader = new BufferedReader(
                         new InputStreamReader(gzipInput, charset != null ? charset : StandardCharsets.UTF_8))) {
                return reader.lines().reduce("", (a, b) -> a + b);
            }
        }
        return new String(body, charset != null ? charset : StandardCharsets.UTF_8);
    }

    /**
     * 从Content-Type解析字符集
     */
    private Charset parseCharset(String contentType) {
        if (contentType == null) {
            return null;
        }

        String[] parts = contentType.split(";");
        for (String part : parts) {
            if (part.trim().startsWith("charset=")) {
                try {
                    return Charset.forName(part.split("=")[1].trim());
                } catch (Exception e) {
                    log.error("无效的字符集声明: {}", part);
                }
            }
        }
        return null;
    }

    /**
     * 处理客户端错误（4xx响应）
     */
    private ResponseEntity<byte[]> handleClientError(UUID uuid, long startTime, HttpClientErrorException e,
                                                     String targetUrl, HttpMethod method,
                                                     String body, String formParamsString) {
        byte[] responseBodyBytes = e.getResponseBodyAsByteArray();
        String errorMessage = new String(responseBodyBytes, StandardCharsets.UTF_8);

        // 记录客户端错误日志
        log.error("请求转发失败(响应非200) | uuid:{} | 目标URL: {} | 方法: {} | 请求数据body: {} | 请求数据formParams: {} | 原因: {} | 错误响应体: {} | 程序处理耗时: {} ms",
                uuid, targetUrl, method, body, formParamsString, e.getMessage(), errorMessage, System.currentTimeMillis() - startTime);

        // 特殊处理401未授权
        HttpStatus statusCode = e.getStatusCode();
        if (statusCode == HttpStatus.UNAUTHORIZED) {
            /*
               对于401错误，直接返回相应的状态码和消息
               解释：401 Unauthorized 是一个特殊的 HTTP 状态码，表示客户端请求需要用户认证。在某些情况下，401 响应可能没有响应体，或者响应体格式不符合预期。
               因此，单独处理 401 响应可以确保在遇到这种情况时，代码能够正确处理并返回适当的响应，而不是尝试处理可能不存在或格式不正确的响应体。
             */
            return ResponseEntity.status(statusCode)
                    .body(e.getResponseBodyAsString().getBytes(StandardCharsets.UTF_8));
        }
        return ResponseEntity.status(statusCode)
                .headers(e.getResponseHeaders())
                .body(processErrorBody(e.getResponseBodyAsByteArray(),
                        e.getResponseHeaders()));
    }

    /**
     * 处理服务端错误（5xx响应）
     */
    private ResponseEntity<byte[]> handleServerError(UUID uuid, long startTime, Throwable e,
                                                     String targetUrl, HttpMethod method,
                                                     String body, String formParamsString) {
        log.error("服务器处理失败 | uuid:{} | 目标URL: {} | 方法: {} | 请求数据body: {} | 请求数据formParams: {} | 原因: {} | 程序处理耗时: {} ms",
                uuid, targetUrl, method, body, formParamsString, e.getMessage(), System.currentTimeMillis() - startTime, e);
        return ResponseEntity.internalServerError()
                .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE + ";charset=UTF-8")
                .body(("服务器处理失败: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 构造标准错误响应
     */
    private ResponseEntity<byte[]> buildErrorResponse(UUID uuid, long startTime,
                                                      Exception cause) {
        String message = cause.getMessage();
        log.error("请求错误 | UUID:{} | 状态:{} | 原因: {} | 程序处理耗时: {} ms", uuid, HttpStatus.BAD_REQUEST, message, System.currentTimeMillis() - startTime);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE + ";charset=UTF-8")
                .body(message.getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 处理错误响应正文
     */
    private byte[] processErrorBody(byte[] body, HttpHeaders headers) {
        if (body == null || body.length == 0) {
            return new byte[0];
        }

        String contentType = headers.getFirst(HttpHeaders.CONTENT_TYPE);
        // 仅处理文本类型错误信息
        if (contentType != null && contentType.startsWith(TEXT)) {
            Charset charset = parseCharset(contentType);
            charset = charset != null ? charset : StandardCharsets.UTF_8;
            return new String(body, charset)
                    .getBytes(charset);
        }
        return body;
    }
}
